package jdesktop.drag;

import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.StringSelection;
import java.awt.dnd.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

// currently work swith a simple drag on the content page of a frame.
// Should also work with a nested component, and that component, or a container may end up moving.

public class DragListener implements MouseListener, MouseMotionListener, DragGestureListener,
        DragSourceListener, DragSourceMotionListener
{
    private DragCoordinator coordinator;

    public DragListener(DragCoordinator coordinator)
    {
        this.coordinator = coordinator;
    }

    static class DragCoordinator
    {
        private Component   component;
        private DragBounds  reshaper;

        DragCoordinator(Component component, DragBounds reshaper)
        {
            this.component = component;
            this.reshaper = reshaper;
        }

        public Component getComponent()
        {
            return component;
        }

        public DragBounds getReshaper()
        {
            return reshaper;
        }

        public void dragStarted(Point startPoint)
        {
            reshaper.init(component.getBounds(), startPoint);
        }

        public void dragMoved(Point pt)
        {
            setBounds(reshaper.computeBounds(pt));
        }

        public void dragEnd(Point pt)
        {
            setBounds(reshaper.computeBounds(pt));
        }

        public void dragExit()
        {
            setBounds(reshaper.startBounds);
        }

        private void setBounds(Rectangle bounds)
        {
            component.setBounds(bounds);
        }

        public Rectangle constrainBounds(Rectangle bounds)
        {
            reshaper.fixupBounds(bounds);
            return bounds;
        }
    }


    public static class DragBounds
    {
        private Rectangle   newBounds = new Rectangle();

        private Rectangle   startBounds;
        private Point       startClick;

        private final BoundsConstraint constraint;

        public DragBounds(BoundsConstraint constraint)
        {
            this.constraint = constraint;
        }

        public void init(Rectangle startBounds, Point startClick)
        {
            this.startBounds = startBounds;
            this.startClick = startClick;
        }

        public Rectangle computeBounds(Point currentDrag)
        {
            return dragBounds(newBounds, currentDrag, this.startBounds, this.startClick);
        }

        /**
         * Computes the new bounds for the window based on the drag position.
         * @param newBounds A rectangle to receive the new bounds.
         * @param currentDrag   The current drag position, in absolute screen co-ordinates.
         * @param startBounds   The starting bounds of the window at the start of the drag/
         * @param startClick    The starting click position.
         * @return  newBounds, filled with the new bounds for the object.
         */
        public Rectangle dragBounds(Rectangle newBounds, Point currentDrag, Rectangle startBounds, Point startClick)
        {
            newBounds.setBounds(startBounds);
            newBounds.x += currentDrag.x-startClick.x;
            newBounds.y += currentDrag.y-startClick.y;
            return fixupBounds(newBounds);
        }

        protected Rectangle fixupBounds(Rectangle newBounds)
        {
            constraint.constrain(newBounds);
            return newBounds;
        }
    }

    public class DragLocation
    {
        /**
         * The start position of the window at the start of the drag
         */
        private Point   startPosition;

        /**
         * The start click position of the drag.
         */
        private Point   startClick;

        private Point   newLocation;

        public DragLocation(Point startPosition, Point startClick)
        {
            this.startPosition = startPosition;
            this.startClick = startClick;
            this.newLocation = new Point();
        }

        /**
         * Computes the new location of the widnow based on the current drag position.
         * @param currentDrag
         * @return
         */
        public Point computeLocation(Point currentDrag)
        {
            return dragLocation(newLocation, currentDrag, this.startPosition, this.startClick);
        }

        /**
         * Compute the new location of the window
         */
        public Point dragLocation(Point target, Point currentDrag, Point originalLoction, Point startDrag)
        {
            target.x = (startDrag.x-currentDrag.x)+originalLoction.x;
            target.y = (startDrag.y-currentDrag.y)+originalLoction.y;
            return fixupLocation(target);
        }

        protected Point fixupLocation(Point pt)
        {
            // no-op leave as is
            return pt;
        }
    }



    public static DragListener install(JFrame frame, BoundsConstraint constraint)
    {
        DragListener existing = getDragListener(frame);
        if (existing!=null)
            throw new IllegalStateException();

        DragListener l = new DragListener(new DragCoordinator(frame, new DragBounds(constraint)));
        JRootPane pane = frame.getRootPane();
        pane.putClientProperty(DragListener.class, l);
        frame.addMouseListener(l);
        frame.addMouseMotionListener(l);
        //DragSource source = DragSource.getDefaultDragSource();
        //source.addDragSourceListener(l);
        //source.addDragSourceMotionListener(l);
        //DragGestureRecognizer dgr = source.createDefaultDragGestureRecognizer(frame.getContentPane(), DnDConstants.ACTION_MOVE, l);

        // tdo - the constraints on the bounds should be done by constraining the set bounds on the component,
        // so the component will always be within constrained bounds.
        frame.setBounds(l.coordinator.constrainBounds(frame.getBounds()));
        return l;
    }

    public static void uninstall(JFrame frame)
    {
        DragListener l = getDragListener(frame);
        if (l!=null)
            l.uninstallFromFrame(frame);
    }

    private static DragListener getDragListener(JFrame frame)
    {
        return (DragListener) frame.getRootPane().getClientProperty(DragListener.class);
    }

    public void uninstallFromFrame(JFrame frame)
    {
        frame.removeMouseMotionListener(this);
        frame.removeMouseListener(this);
    }

    public void mouseDragged(MouseEvent e)
    {
        coordinator.dragMoved(e.getLocationOnScreen());
    }

    public void mouseClicked(MouseEvent e)
    {
    }

    public void mousePressed(MouseEvent e)
    {
        coordinator.dragStarted(e.getLocationOnScreen());
    }

    public void mouseReleased(MouseEvent e)
    {
        coordinator.dragEnd(e.getLocationOnScreen());
    }

    public void mouseEntered(MouseEvent e)
    {
    }

    public void mouseExited(MouseEvent e)
    {
    }

    public void mouseMoved(MouseEvent e)
    {        
    }


    public void dragGestureRecognized(DragGestureEvent dge)
    {
        coordinator.dragStarted(dge.getDragOrigin());
        Transferable tx = new StringSelection("");
        dge.startDrag(null, tx);
    }

    public void dragEnter(DragSourceDragEvent dsde)
    {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    public void dragOver(DragSourceDragEvent dsde)
    {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    public void dropActionChanged(DragSourceDragEvent dsde)
    {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    public void dragExit(DragSourceEvent dse)
    {
        coordinator.dragExit();
    }

    public void dragDropEnd(DragSourceDropEvent dsde)
    {
        coordinator.dragEnd(dsde.getLocation());
    }

    public void dragMouseMoved(DragSourceDragEvent dsde)
    {
        coordinator.dragMoved(dsde.getLocation());
    }

}
